package ca.appspace.sac;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class SacParser {

	private List<NewLineCallback> _newLineCallbacks = new ArrayList<NewLineCallback>();
	private List<ValueParsedCallback> _commonValueParsedCallbacks = new ArrayList<ValueParsedCallback>();
	private Map<Integer, List<ValueParsedCallback>> _columnValueParsedCallbacks = new HashMap<Integer, List<ValueParsedCallback>>();

	private int _bufferSize = 8094;
	private char _columnSeparator = ',';
	private char _escapeCharacter = '"';
	private boolean _trimValues = true;

	/**
	 * Set character used to separate columns. Default is comma (,)
	 * @param ch 
	 */
	public void setColumnSeparator(char ch) {
		_columnSeparator = ch;
	}

	/**
	 * Set character used to escape text values. I.e. when you have comma as a separator but you need 
	 * to store value "test , test, test", you would escape it with escape character
	 * @param ch 
	 */
	public void setEscapeCharacter(char ch) {
		_escapeCharacter = ch;
	}
	
	/**
	 * Buffer size in bytes for reading. Default value is 8094. 
	 * @param newSize values greater then 0
	 */
	public void setBufferSize(int newSize) {
		if (newSize>0) {
			_bufferSize = newSize;
		}
	}
	
	/**
	 * All added callbacks will be called every time new line is reached
	 * @param newLineCallback
	 */
	public void addNewLineCallback(NewLineCallback newLineCallback) {
		if (newLineCallback!=null) {
			_newLineCallbacks.add(newLineCallback);
		}
	}

	/**
	 * All callbacks added using this method will be called on every single parsed value
	 * @param valueParsedCallback
	 */
	public void addValueParsedCallback(ValueParsedCallback valueParsedCallback) {
		_commonValueParsedCallbacks.add(valueParsedCallback);
	}

	/**
	 * All callbacks added using this method will be called on particular column value parsed
	 * @param position
	 * @param valueParsedCallback
	 */
	public void addValueParsedCallback(int position, ValueParsedCallback valueParsedCallback) {
		List<ValueParsedCallback> existingList = _columnValueParsedCallbacks.get(position);
		if (existingList==null) {
			existingList = new ArrayList<ValueParsedCallback>();
			_columnValueParsedCallbacks.put(position, existingList);
		}
		existingList.add(valueParsedCallback);
	}

	/**
	 * Parse reader pointing to CSV content. 
	 * Easiest way of parsing a CSV file is to pass new FileReader(pathToTheFile)
	 * @param reader
	 * @throws IOException
	 */
	public void parse(Reader reader) throws IOException {
		BufferedReader bufferedReader = null;
		try {
			bufferedReader = new BufferedReader(reader, _bufferSize);
			String line = null;
			int lineNumber = 1;
			while ( (line = bufferedReader.readLine())!=null) {
				parseLine(lineNumber, line);
				lineNumber++;
			}
		} finally {
			if (bufferedReader!=null) {
				try {
					bufferedReader.close();
				} catch (Exception e) {}
			};
		}
	}

	private void parseLine(int lineNumber, String line) {
		boolean escaped = false;
		StringBuffer sb = new StringBuffer();
		for (NewLineCallback newLineCallback : _newLineCallbacks) {
			newLineCallback.onNewLineStarted(lineNumber);
		}
		int columnNumber = 1;
		for (int i=0; i<line.length(); i++) {
			char ch = line.charAt(i);
			if (_columnSeparator==ch) {
				if (!escaped) {
					valueParsed(columnNumber, sb.toString());
					sb.setLength(0);
					columnNumber++;
				}
				continue;
			} else if (_escapeCharacter==ch) {
				escaped = !escaped;
				continue;
			} else {
				sb.append(ch);
			}
		}
	}

	private void valueParsed(int columnNumber, String value) {
		if (_trimValues) {
			value = value.trim();
		}
		for (ValueParsedCallback callback : _commonValueParsedCallbacks) {
			callback.onNewFieldParsed(columnNumber, value);
		}
		List<ValueParsedCallback> columnCallbacks = _columnValueParsedCallbacks.get(columnNumber);
		if (columnCallbacks==null || columnCallbacks.isEmpty()) {
			return;
		}
		for (ValueParsedCallback callback : columnCallbacks) {
			callback.onNewFieldParsed(columnNumber, value);
		}
	}
}
